/*
Copyright (C) 2012 Eric Lasota
Copyright (C) 1996-1997 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/
#include <time.h>

#include "quakedef.h"
#include "mtrand.h"
#include "rr.h"
#include "sha256.h"

extern kbutton_t	in_jump, in_attack;
extern int in_impulse;

typedef union
{
	int i;
	float f;
} rr_intfloat_t;

cvar_t	rr_frametime = { "rr_frametime", "0x3c6a2000" };
cvar_t	rr_timescale = { "rr_timescale", "1.0" };
cvar_t	rr_framelimit = { "rr_framelimit", "-1" };
cvar_t	rr_randomseed = { "rr_randomseed", "0" };
cvar_t	rr_dead = { "rr_dead", "0" };
cvar_t	rr_nomapchange = { "rr_nomapchange", "0", true };
cvar_t	rr_machineid = { "rr_machineid", "", true };
cvar_t	rr_record = { "rr_record", "0" };
cvar_t	rr_stringpoolsize = { "rr_stringpoolsize", "100000" };

mtrand_state_t rr_randstate;
rr_q1_sessiondata_t rr_sessiondata;
int rr_framenumber;
char *rr_stringpool;
int rr_stringpoolsizei;
int rr_stringpooloffset;
qboolean rr_clienthascontrol;

char		rr_inputfilename[MAX_OSPATH];
FILE		*rr_inputfile = NULL;
qboolean	rr_allowrecord = false;
qboolean	rr_recording = false;

typedef struct
{
	time_t t;
	clock_t c;
} rr_machineid_hash_t;

void RR_UIntToChars(unsigned int ui, char *outString)
{
	const char *nibbles = "0123456789abcdef";
	int n;
	for(n=0;n<8;n++)
		outString[7-n] = nibbles[(ui >> (n * 4)) & 0xf];
}


void RR_GenerateMachineID (void)
{
	rr_machineid_hash_t midh;
	unsigned int hashdata[8];
	int i;
	char hashchars[65];

	if(rr_machineid.string[0] != '\0')
		return;

	time(&midh.t);
	midh.c = clock();

	sha256(hashdata, &midh, sizeof(midh));
	for(i=0;i<8;i++)
		RR_UIntToChars(hashdata[i], hashchars + i*8);
	hashchars[64] = '\0';
	Cvar_Set("rr_machineid", hashchars);
}

int RR_Hex32(char *str)
{
	int rv = 0;
	int i;
	if(str[0] != '0' || str[1] != 'x')
	{
		int rv = 0;
		int negate = 0;
		if(str[0] == '-')
		{
			str++;
			negate = 1;
		}
		while(1)
		{
			if(*str >= '0' && *str <= '9')
			{
				rv = rv * 10;
				if(negate)
					rv -= (*str) - '0';
				else
					rv += (*str) - '0';
			}
			else
				return rv;
			str++;
		}
	}

	for(i=0;;i++)
	{
		char c = str[i+2];
		int cv;
		if(c >= '0' && c <= '9')
			cv = c - '0';
		else if(c >= 'a' && c <= 'f')
			cv = c + 0xA - 'a';
		else if(c >= 'A' && c <= 'F')
			cv = c + 0xA - 'A';
		else if(c == '\0')
			return rv;
		else
			return atoi(str);
		rv = ((rv << 4) | cv);
	}
	return rv;
}

int RR_RecastFToI(float f)
{
	rr_intfloat_t intfloat;
	intfloat.f = f;
	return intfloat.i;
}

float RR_RecastIToF(int i)
{
	rr_intfloat_t intfloat;
	intfloat.i = i;
	return intfloat.f;
}

void RR_LaunchSession (unsigned int randomSeed, qboolean closeInput)
{
	int i;
	char mapname[sizeof(rr_sessiondata.mapname)];

	Con_Printf ("Launching session...\n\n");

	// NOTE: The session will be destroyed by SpawnServer, so anything that could wind up in the session needs to be shuttled off before then
	// This process needs to be lossless to avoid desyncs too

	Cvar_SetValue("skill", rr_sessiondata.skill);

	mtrand_seed(&rr_randstate, randomSeed);

	// Reload spawn parameters and flags
	svs.serverflags = rr_sessiondata.serverflags;
	for(i=0;i<NUM_SPAWN_PARMS;i++)
		svs.clients[0].spawn_parms[i] = rr_sessiondata.spawnparms[i];
	svs.serverflags = rr_sessiondata.serverflags;
	memcpy(mapname, rr_sessiondata.mapname, sizeof(mapname));
	rr_clienthascontrol = false;

	SV_SpawnServer (mapname, closeInput);
}

void RR_CloseInput_f (void)
{
	if(rr_inputfile)
	{
		Con_Printf("Closing input file\n");
		fclose(rr_inputfile);
		rr_inputfile = NULL;
	}
	rr_allowrecord = false;
	rr_recording = false;
}

static qboolean RR_StreamCopyInput(FILE *srcF, FILE *destF, size_t sz, qboolean increment_rr)
{
	char buf[1024];

	if(increment_rr)
	{
		rr_q1_inputfile_t header;
		if(!fread(&header, sizeof(header), 1, srcF))
			return false;
		header.numrecords++;
		fwrite(&header, sizeof(header), 1, destF);
		sz -= sizeof(header);
	}

	while(sz >= sizeof(buf))
	{
		if(!fread(buf, sizeof(buf), 1, srcF))
			return false;
		if(!fwrite(buf, sizeof(buf), 1, destF))
			return false;
		sz -= sizeof(buf);
	}

	if(sz)
	{
		if(!fread(buf, sz, 1, srcF))
			return false;
		if(!fwrite(buf, sz, 1, destF))
			return false;
	}

	return true;

}

qboolean RR_TruncatedCopy (char *srcFileName, char *destFileName, size_t sz, qboolean increment_rr)
{
	FILE *f1, *f2;
	qboolean succeeded;

	f1 = fopen(srcFileName, "rb");
	if(!f1)
		return false;
	f2 = fopen(destFileName, "w+b");
	if(!f2)
	{
		fclose(f1);
		return false;
	}

	succeeded = RR_StreamCopyInput(f1, f2, sz, increment_rr);

	fclose(f1);
	fclose(f2);

	return succeeded;
}

void RR_ProcessInput (usercmd_t *ucmd)
{
	RR_GenerateMachineID ();
	rr_keybits = 0;

	if(rr_framelimit.value == 0.0f)
		return;

	// Stock game cycles the RNG every frame, do that here to prevent any artifacts with frame advance
	RR_Rand ();

	if(!rr_inputfile)
	{
		if(rr_record.value)
		{
			Con_Printf("Can't record with no input file open\n");
			Cvar_SetValue("rr_record", 0.0f);
		}
		return;
	}

	if(rr_record.value)
	{
		if(!rr_allowrecord)
		{
			Con_Printf("Input file was not opened in edit mode, can't record\n");
			Cvar_SetValue("rr_record", 0.0f);
			return;
		}

		if(!rr_recording)
		{
			char tempname[MAX_OSPATH];
			size_t cutpoint = ftell(rr_inputfile);

			sprintf(tempname, "%s/temp.qri", com_gamedir);
			RR_CloseInput_f();

			if(!RR_TruncatedCopy(rr_inputfilename, tempname, cutpoint, true) ||
				remove(rr_inputfilename) ||
				rename(tempname, rr_inputfilename))
			{
				Con_Printf("Something went wrong truncating the input file!\n");
				return;
			}

			Con_Printf("Resuming record from current frame\n");

			rr_inputfile = fopen(rr_inputfilename, "ab");
			if(!rr_inputfile)
			{
				Con_Printf("Could not reopen input file\n");
				return;
			}
			fseek(rr_inputfile, 0, SEEK_END);
			rr_allowrecord = true;
			rr_recording = true;
		}

		// Construct an output frame
		{
			rr_q1_inputframe_t iframe;
			unsigned char buttonbits = 0;

			VectorCopy(cl.viewangles, iframe.viewangles);
			iframe.forwardmove = (short)ucmd->forwardmove;
			iframe.sidemove = (short)ucmd->sidemove;
			iframe.upmove = (short)ucmd->upmove;

			if (in_jump.state & 3)
				buttonbits |= RR_Q1_BIT_JUMP;
			if (in_attack.state & 3)
				buttonbits |= RR_Q1_BIT_ATTACK;
			iframe.buttonbits = buttonbits;

			iframe.impulse = (unsigned char)in_impulse;

			// Write out
			fwrite(&iframe, sizeof(iframe), 1, rr_inputfile);
		}
	}
	else
	{
		if(rr_recording)
		{
			// Close the current input
			RR_CloseInput_f();
			return;
		}

		// Try to read a frame
		{
			rr_q1_inputframe_t iframe;
			long inputloc = ftell(rr_inputfile);
			int eof = feof(rr_inputfile);

			if(!fread(&iframe, sizeof(iframe), 1, rr_inputfile))
			{
				RR_CloseInput_f();
				return;
			}

			// Parse all
			VectorCopy(iframe.viewangles, cl.viewangles);
			ucmd->forwardmove = iframe.forwardmove;
			ucmd->sidemove = iframe.sidemove;
			ucmd->upmove = iframe.upmove;
			// Parse bits

			rr_keybits = iframe.buttonbits;

			in_impulse = iframe.impulse;
		}
	}

	rr_framenumber++;
}

void RR_OpenInput_f (void)
{
	unsigned int sessionHash[8];
	char	filename[MAX_OSPATH];
	FILE	*f;
	int i;
	qboolean allowmismatch = false;
	qboolean edit = false;
	qboolean record = false;
	rr_q1_inputfile_t inputheader;

	sha256(sessionHash, &rr_sessiondata, sizeof(rr_sessiondata));

	if (Cmd_Argc() < 2)
	{
		Con_Printf ("rr_openinput <input> ['allowmismatch'] ['edit'] ['record']: Opens an input file and restarts the session\n");
		Con_Printf ("    allowmismatch: Allow even if the current session doesn't match\n");
		Con_Printf ("    edit: Allow recording with this input file\n");
		Con_Printf ("    record: Creates a new input file, wipes any existing input\n\n");
		return;
	}
	
	if (!sv.active || cls.demoplayback)
	{
		Con_Printf ("Input can only be opened while the server is running\n");
		return;
	}

	for(i=2;i<Cmd_Argc();i++)
	{
		char *str = Cmd_Argv(i);
		if(!strcmp(str, "allowmismatch"))
			allowmismatch = true;
		else if(!strcmp(str, "record"))
			record = true;
		else if(!strcmp(str, "edit"))
			edit = true;
		else
			Con_Printf("Unknown option %s\n", Cmd_Argv(i));
	}
	
	sprintf(filename, "%s/%s.qri", com_gamedir, Cmd_Argv(1));

	if(record)
	{
		edit = true;

		f = fopen(filename, "w+b");
		if(!f)
		{
			Con_Printf ("Could not open %s\n", filename);
			return;
		}

		memcpy(inputheader.identifier, RR_Q1_INPUTFILE_IDENTIFIER, sizeof(inputheader.identifier));
		inputheader.initialseed = RR_Hex32(rr_randomseed.string);
		inputheader.numrecords = 0;
		memcpy(inputheader.sessionhash, sessionHash, sizeof(sessionHash));
		inputheader.version = RR_Q1_INPUTFILE_VERSION;
		inputheader.framesize = sizeof(rr_q1_inputframe_t);
		inputheader.headersize = sizeof(inputheader);
		inputheader.frametime = RR_RecastIToF(RR_Hex32(rr_frametime.string));

		fwrite(&inputheader, sizeof(inputheader), 1, f);

		rr_inputfile = f;
		rr_recording = true;
		rr_allowrecord = true;
		Cvar_SetValue("rr_record", 1.0f);
	}
	else
	{
		int nRead;

		f = fopen(filename, "rb");

		if(!f)
		{
			Con_Printf ("Could not open %s\n", filename);
			return;
		}

		nRead = fread(&inputheader, sizeof(inputheader), 1, f);

		if(nRead != 1
			|| inputheader.version != RR_Q1_INPUTFILE_VERSION
			|| inputheader.framesize != sizeof(rr_q1_inputframe_t)
			|| inputheader.headersize != sizeof(inputheader)
			|| memcmp(inputheader.identifier, RR_Q1_INPUTFILE_IDENTIFIER, sizeof(inputheader.identifier))
			)
		{
			Con_Printf ("Input file was the wrong version\n");
			fclose(f);
			return;
		}

		if(memcmp(inputheader.sessionhash, sessionHash, sizeof(sessionHash)))
		{
			if(!allowmismatch)
			{
				Con_Printf ("Input file might not be compatible with this session.  Specify 'allowmismatch' to allow anyway.\n");
				fclose(f);
				return;
			}
			else
			{
				Con_Printf ("Using altered session data.\n");
				memcpy(inputheader.sessionhash, sessionHash, sizeof(sessionHash));
			}
		}

		rr_inputfile = f;
		rr_recording = false;
		rr_allowrecord = edit;
		Cvar_SetValue("rr_record", 0.0f);

		// Set the frame time
		{
			char newframetime[11];
			newframetime[10] = '\0';
			newframetime[0] = '0';
			newframetime[1] = 'x';

			RR_UIntToChars(RR_RecastFToI(inputheader.frametime), newframetime+2);
			Cvar_Set("rr_frametime", newframetime);
		}
	}

	Con_Printf("Input file %s opened\n", filename);
	strcpy(rr_inputfilename, filename);

	rr_framenumber = 0;
	RR_LaunchSession(inputheader.initialseed, false);
}

void RR_OpenSession_f (void)
{
	char	filename[MAX_OSPATH];

	if (Cmd_Argc() < 2)
	{
		Con_Printf ("rr_opensession <session> : Load an existing session\n");
		return;
	}
	if (!sv.active || cls.demoplayback)
	{
		Con_Printf ("Sessions can only be opened while the server is running\n");
		return;
	}

	
	sprintf(filename, "%s/%s.qrs", com_gamedir, Cmd_Argv(1));
	{
		FILE *f = fopen(filename, "rb");
		int i;
		rr_q1_sessiondata_t sess;

		if(!f)
		{
			Con_Printf ("Could not open session %s\n", filename);
			return;
		}
		
		fread(&sess, sizeof(sess), 1, f);
		fclose(f);

		if(sess.mapname[sizeof(sess.mapname)-1] != '\0')
		{
			Con_Printf ("Session data is corrupt\n");
			return;
		}
		if(sess.version != RR_Q1_SESSIONDATA_VERSION)
		{
			Con_Printf ("Session is version %i, expected %i\n", sess.version, RR_Q1_SESSIONDATA_VERSION);
			return;
		}

		memcpy(&rr_sessiondata, &sess, sizeof(rr_sessiondata));
		RR_LaunchSession(RR_Hex32(rr_randomseed.string), true);
	}
}

// rr_newsession <session file> ["overwrite"]
void RR_NewSession_f (void)
{
	int optionIndex;
	qboolean allowOverwrite = false;
	char	fullname[MAX_OSPATH];

	int argc = Cmd_Argc();
	if(argc < 2 || strlen(Cmd_Argv(1)) > 32)
	{
		Con_Printf("Usage: rr_newsession <session file> ['overwrite']\n");
		return;
	}

	for(optionIndex=2;optionIndex<argc;optionIndex++)
	{
		if(!Q_strcmp(Cmd_Argv(optionIndex), "overwrite"))
			allowOverwrite = true;
	}
	
	sprintf(fullname, "%s/%s.qrs", com_gamedir, Cmd_Argv(1));

	if(!allowOverwrite)
	{
		FILE *f = fopen(fullname, "rb");
		if(f)
		{
			fclose(f);
			Con_Printf("ERROR: Session file %s already exists, use 'overwrite' to force\n", fullname);
			return;
		}
	}

	{
		FILE *f = fopen(fullname, "w+b");
		int i;

		if(!f)
		{
			Con_Printf("ERROR: Couldn't open session file %s\n", fullname);
			return;
		}

		fwrite(&rr_sessiondata, sizeof(rr_sessiondata), 1, f);

		fclose(f);
		Con_Printf("Saved session to %s\n", fullname);
	}
}

static void RR_WriteEval(eval_t *eval, int evalType, FILE *f)
{
	int slen;

	switch(evalType)
	{
	case ev_string:
		if(eval->string == 0)
			slen = -1;
		else
			slen = strlen(pr_strings + eval->string);
		fwrite(&slen, sizeof(slen), 1, f);
		if(slen != -1)
			fwrite(pr_strings + eval->string, slen, 1, f);
		break;
	case ev_entity:
		fwrite(&eval->edict, sizeof(eval->edict), 1, f);
		break;
	case ev_function:
		fwrite(&eval->function, sizeof(eval->function), 1, f);
		break;
	case ev_field:
		fwrite(&eval->_int, sizeof(eval->_int), 1, f);
		break;
	case ev_void:
		break;
	case ev_float:
		fwrite(&eval->_float, sizeof(eval->_float), 1, f);
		break;
	case ev_vector:
		fwrite(&eval->vector, sizeof(eval->vector), 1, f);
		break;
	}
}


static void RR_ReadEval(eval_t *eval, int evalType, FILE *f)
{
	int slen;
	char *scratch;

	switch(evalType)
	{
	case ev_string:
		fread(&slen, sizeof(slen), 1, f);

		if(slen >= 0)
		{
			if(rr_stringpoolsizei - rr_stringpooloffset < slen+1)
				Sys_Error("State string pool was exhausted\n");

			scratch = rr_stringpool + rr_stringpooloffset;
			rr_stringpooloffset += slen + 1;

			fread(scratch, slen, 1, f);
			scratch[slen] = '\0';

			eval->string = scratch - pr_strings;
		}
		else
			eval->string = 0;
		break;
	case ev_entity:
		fread(&eval->edict, sizeof(eval->edict), 1, f);
		break;
	case ev_function:
		fread(&eval->function, sizeof(eval->function), 1, f);
		break;
	case ev_field:
		fread(&eval->_int, sizeof(eval->_int), 1, f);
		break;
	case ev_void:
		break;
	case ev_float:
		fread(&eval->_float, sizeof(eval->_float), 1, f);
		break;
	case ev_vector:
		fread(&eval->vector, sizeof(eval->vector), 1, f);
		break;
	}
}

void RR_DumpEdictLinks()
{
	int i;
	for(i=0;i<sv.num_edicts;i++)
	{
		edict_t *edict = EDICT_NUM(i);
		Con_Printf("ED %i links: %x %x\n", i, edict->area.prev, edict->area.next);
	}
}

void RR_SaveState_f (void)
{
	char	fullname[MAX_OSPATH];
	FILE *f;
	unsigned int nInput = 0;

	int argc = Cmd_Argc();
	if(argc < 2)
	{
		Con_Printf("Usage: rr_savestate <state file> : Saves the current game state\n");
		return;
	}
	
	
	if (!sv.active || cls.demoplayback)
	{
		Con_Printf ("States can only be saved while the server is running\n");
		return;
	}

	sprintf(fullname, "%s/%s.qrst", com_gamedir, Cmd_Argv(1));

	f = fopen(fullname, "w+b");
	if(!f)
	{
		Con_Printf("Could not open %s for write\n", fullname);
		return;
	}

	if(rr_inputfile)
		nInput = ftell(rr_inputfile);

	{
		rr_q1_savestate_t header;

		RR_GenerateMachineID();

		header.version = RR_Q1_SAVESTATE_VERSION;
		header.rrFrameCount = rr_framenumber;
		header.rrNumInputBytes = nInput;
		memcpy(&header.rrRandomState, &rr_randstate, sizeof(rr_randstate));
		if(strlen(rr_machineid.string) < sizeof(header.machineId))
		{
			memset(header.machineId, 0, sizeof(header.machineId));
			strcpy(header.machineId, rr_machineid.string);
		}
		else
			memcpy(header.machineId, rr_machineid.string, sizeof(header.machineId));

		sha256(header.sessionHash, &rr_sessiondata, sizeof(rr_sessiondata));

		header.cvSkill = skill.value;
		header.svTime = sv.time;
		memset(header.svName, 0, sizeof(header.svName));
		strcpy(header.svName, sv.name);
		header.svMaxClients = svs.maxclients;
		header.svMaxClientsLimit = svs.maxclientslimit;
		header.svNumEdicts = sv.num_edicts;
		header.svNumAreaNodes = SV_NumAreaNodes();
		header.prNumGlobalDefs = progs->numglobaldefs;
		header.prNumFieldDefs = progs->numfielddefs;
		VectorCopy(cl.viewangles, header.clViewAngles);

		fwrite(&header, sizeof(header), 1, f);
	}

	// Copy input
	if(nInput)
	{
		long inputsize = ftell(rr_inputfile);
		fseek(rr_inputfile, 0, SEEK_SET);
		RR_StreamCopyInput(rr_inputfile, f, nInput, false);
		fseek(rr_inputfile, nInput, SEEK_SET);
	}

	// Copy client states
	{
		int i;
		long clientstateoffset = ftell(f);

		for(i=0;i<svs.maxclients;i++)
		{
			rr_q1_clientstate_t clstate;
			clstate.colors = svs.clients[i].colors;
			clstate.edictIdx = EDICT_TO_PROG(svs.clients[i].edict);
			memset(clstate.name, 0, sizeof(clstate.name));
			strcpy(clstate.name, svs.clients[i].name);
			memcpy(clstate.spawnParms, svs.clients[i].spawn_parms, sizeof(clstate.spawnParms));

			fwrite(&clstate, sizeof(clstate), 1, f);
		}
	}

	// Write globals
	{
		int i;
		for(i=0;i<progs->numglobaldefs;i++)
		{
			ddef_t *global = &pr_globaldefs[i];
			eval_t *evalp;
			int gtype;

			if(!(global->type & DEF_SAVEGLOBAL))
				continue;
			gtype = global->type ^ DEF_SAVEGLOBAL;

			evalp = (eval_t *)(pr_globals + global->ofs);
			RR_WriteEval(evalp, gtype, f);
		}
	}

	// Write all edict states (including unallocated ones!)
	{
		int i;
		for(i=0;i<MAX_EDICTS;i++)
		{
			rr_q1_edictstate_t estate;
			edict_t *edict = EDICT_NUM(i);

			memcpy(&estate.baseline, &edict->baseline, sizeof(entity_state_t));
			estate.free = edict->free;
			estate.freetime = edict->freetime;
			estate.num_leafs = edict->num_leafs;
			memcpy(estate.leafnums, edict->leafnums, sizeof(estate.leafnums));

			fwrite(&estate, sizeof(estate), 1, f);

			if(!estate.free)
			{
				int fieldnum;
				for(fieldnum=0;fieldnum<progs->numfielddefs;fieldnum++)
				{
					ddef_t *fdef = pr_fielddefs + fieldnum;
					void *dat = ((char *)(&edict->v) + fdef->ofs*4);
					RR_WriteEval((eval_t *)dat, fdef->type, f);
				}
			}
		}
	}

	// Write collision links
	{
		int i, nAreas;
		nAreas = SV_NumAreaNodes();
		for(i=0;i<nAreas;i++)
			SV_SaveAreaNode(i, f);
	}

	fclose(f);

	Con_Printf("Saved state %s\n", fullname);
}

void RR_InitStringPool (void)
{
	int spBytes = atoi(rr_stringpoolsize.string);
	if(spBytes < 10 || spBytes > 0x1000000)
		Sys_Error("String pool byte count is out of range: %i\n", spBytes);
	rr_stringpool = (char *)Hunk_Alloc(spBytes);
	rr_stringpoolsizei = spBytes;
	rr_stringpooloffset = 0;
}

void RR_LoadState_f (void)
{
	int i;
	char	fullname[MAX_OSPATH];
	FILE *f;
	unsigned int nInput = 0;
	qboolean allowreroute = false;
	qboolean nosecuritycheck = false;
	rr_q1_savestate_t header;

	int argc = Cmd_Argc();
	if(argc < 2)
	{
		Con_Printf("Usage: rr_loadstate <state file> ['allowreroute']  ['nosecuritycheck'] : Loads a game state.\n");
		Con_Printf("   allowreroute : Allows the state to be used even if the input isn't synced\n");
		Con_Printf("   nosecuritycheck : Disables GUID check, allowing states from other machines\n");
		Con_Printf("                     Loading states from untrusted sources is dangerous and\n");
		Con_Printf("                     may compromise your system's security!\n\n");
		return;
	}
	
	if (!sv.active || cls.demoplayback)
	{
		Con_Printf ("States can only be saved while the server is running\n");
		return;
	}

	for(i=2;i<Cmd_Argc();i++)
	{
		char *str = Cmd_Argv(i);
		if(!strcmp(str, "allowreroute"))
			allowreroute = true;
		else if(!strcmp(str, "nosecuritycheck"))
			nosecuritycheck = true;
		else
			Con_Printf("Unknown option %s\n", Cmd_Argv(i));
	}

	sprintf(fullname, "%s/%s.qrst", com_gamedir, Cmd_Argv(1));
	
	f = fopen(fullname, "rb");
	if(!f)
	{
		Con_Printf("Could not open %s for read\n", fullname);
		return;
	}

	{
		unsigned int sessionHash[8];
		const char *failReason = NULL;

		RR_GenerateMachineID();
		
		sha256(sessionHash, &rr_sessiondata, sizeof(rr_sessiondata));

		if(!fread(&header, sizeof(header), 1, f) ||
			header.version != RR_Q1_SAVESTATE_VERSION)
			failReason = "Couldn't parse header";
		else if(memcmp(header.machineId, rr_machineid.string, sizeof(header.machineId)) && !nosecuritycheck)
			failReason = "Machine ID doesn't match.  WARNING: Loading states from untrusted sources may compromise your system's security!";
		else if(memcmp(header.sessionHash, sessionHash, sizeof(sessionHash)))
			failReason = "State was from a different session";
		else if(memcmp(sv.name, header.svName, sizeof(sv.name)) ||
			svs.maxclients != header.svMaxClients ||
			svs.maxclientslimit != header.svMaxClientsLimit)
			failReason = "Server state (i.e. map, client limit) configuration differs from state";
		else if(SV_NumAreaNodes() != header.svNumAreaNodes ||
			header.prNumGlobalDefs != progs->numglobaldefs ||
			header.prNumFieldDefs != progs->numfielddefs)
			failReason = "World limits (i.e. area nodes, VM state) differ from state";
		else if(rr_inputfile && !rr_allowrecord)
			failReason = "Can't load states while playing unless in edit mode";

		if(failReason)
		{
			fclose(f);
			Con_Printf("Failed to load %s\n%s\n", fullname, failReason);
			return;
		}
	}

	nInput = header.rrNumInputBytes;

	// Figure out what to do with input
	{
		// We're not recording, just skip
		if(!rr_inputfile)
			fseek(f, nInput, SEEK_CUR);		// Not recording, ignore input
		else
		{
			rr_q1_inputfile_t inputheader;
			// Because of an earlier check, if we're here, we're in edit mode
			if(!allowreroute)
			{
				// Verify that we haven't rerouted
				unsigned int currentInputOffset = ftell(rr_inputfile);
				unsigned int syncLength = currentInputOffset;
				unsigned int stateInputOffset = ftell(f);

				if(nInput < syncLength)
					syncLength = nInput;

				// Skip the headers
				fseek(rr_inputfile, sizeof(rr_q1_inputfile_t), SEEK_SET);
				fseek(f, sizeof(rr_q1_inputfile_t), SEEK_CUR);
				syncLength -= sizeof(rr_q1_inputfile_t);

				// Compare input
				while(syncLength)
				{
					char buf1[1024];
					char buf2[1024];
					unsigned int packetLength = syncLength;
					if(packetLength > sizeof(buf1))
						packetLength = sizeof(buf1);
					fread(buf1, packetLength, 1, rr_inputfile);
					fread(buf2, packetLength, 1, f);
					if(memcmp(buf1, buf2, packetLength))
					{
						fseek(rr_inputfile, 0, SEEK_END);
						fclose(f);
						Con_Print("State input sequence does not match the current input.  Use 'allowreroute' to allow the state to overwrite the current input anyway.\n");
						return;
					}
					syncLength -= packetLength;
				}

				fseek(f, stateInputOffset, SEEK_SET);
			}

			// Read the old header and extract rerecord count
			fseek(rr_inputfile, 0, SEEK_SET);
			{
				unsigned int newRecordCount;
				fread(&inputheader, sizeof(inputheader), 1, rr_inputfile);
				newRecordCount = inputheader.numrecords + 1;
				fread(&inputheader, sizeof(inputheader), 1, f);
				inputheader.numrecords = newRecordCount;
			}

			RR_CloseInput_f();
			Con_Printf("Copying input from state file...\n");
			
			rr_inputfile = fopen(rr_inputfilename, "w+b");

			if(!rr_inputfile)
			{
				fclose(f);
				Con_Print("State input sequence does not match the current input.  Use 'allowreroute' to allow the state to overwrite the current input anyway.\n");
				return;
			}

			rr_allowrecord = true;
			rr_recording = false;

			// Export the header
			fwrite(&inputheader, sizeof(inputheader), 1, rr_inputfile);

			{
				// Determine how much input has been passed (there may be remaining input)
				unsigned int targetPoint = sizeof(inputheader) + header.rrFrameCount * sizeof(rr_q1_inputframe_t);

				if(rr_record.value)
				{
					// Only copy the input so far
					rr_recording = true;
					RR_StreamCopyInput(f, rr_inputfile, targetPoint - sizeof(inputheader), false);
					fseek(f, nInput - targetPoint, SEEK_CUR);
				}
				else
				{
					// Not recording, copy all input, then reopen the file since fread on a w+b file will pad the file out with garbage
					RR_StreamCopyInput(f, rr_inputfile, nInput - sizeof(inputheader), false);
					fclose(rr_inputfile);
					rr_inputfile = fopen(rr_inputfilename, "rb");
					fseek(rr_inputfile, targetPoint, SEEK_SET);
				}
			}
		}
	}

	// Start parsing world state data
	rr_framenumber = header.rrFrameCount;
	memcpy(&rr_randstate, &header.rrRandomState, sizeof(rr_randstate));
	Cvar_SetValue("skill", header.cvSkill);
	sv.time = header.svTime;
	sv.num_edicts = header.svNumEdicts;
	VectorCopy(header.clViewAngles, cl.viewangles);

	// Reset strings
	rr_stringpooloffset = 0;

	// Read client states
	{
		int i;
		long clientstateoffset = ftell(f);

		for(i=0;i<svs.maxclients;i++)
		{
			rr_q1_clientstate_t clstate;
			fread(&clstate, sizeof(clstate), 1, f);

			svs.clients[i].colors = clstate.colors;
			svs.clients[i].edict = PROG_TO_EDICT(clstate.edictIdx);
			strcpy(svs.clients[i].name, clstate.name);
			memcpy(svs.clients[i].spawn_parms, clstate.spawnParms, sizeof(clstate.spawnParms));
		}
	}

	// Read globals
	{
		int i;
		for(i=0;i<progs->numglobaldefs;i++)
		{
			ddef_t *global = &pr_globaldefs[i];
			eval_t *evalp;
			int gtype;

			if(!(global->type & DEF_SAVEGLOBAL))
				continue;
			gtype = global->type ^ DEF_SAVEGLOBAL;

			evalp = (eval_t *)(pr_globals + global->ofs);
			RR_ReadEval(evalp, gtype, f);
		}
	}

	// Read all edict states (including unallocated ones!)
	{
		int i;
		for(i=0;i<MAX_EDICTS;i++)
		{
			rr_q1_edictstate_t estate;
			edict_t *edict = EDICT_NUM(i);

			fread(&estate, sizeof(estate), 1, f);

			memcpy(&edict->baseline, &estate.baseline, sizeof(entity_state_t));
			edict->free = (qboolean)estate.free;
			edict->freetime = estate.freetime;
			edict->num_leafs = estate.num_leafs;
			edict->area.next = edict->area.prev = NULL;
			memcpy(edict->leafnums, estate.leafnums, sizeof(estate.leafnums));

			if(!estate.free)
			{
				int fieldnum;
				for(fieldnum=0;fieldnum<progs->numfielddefs;fieldnum++)
				{
					ddef_t *fdef = pr_fielddefs + fieldnum;
					void *dat = ((char *)(&edict->v) + fdef->ofs*4);
					RR_ReadEval((eval_t *)dat, fdef->type, f);
				}
			}
		}
	}

	// Read collision links
	{
		int i, nAreas;
		nAreas = SV_NumAreaNodes();
		for(i=0;i<nAreas;i++)
			SV_LoadAreaNode(i, f);
	}

	fclose(f);

	Con_Printf("Loaded state %s\n", fullname);
}

unsigned int RR_Rand ()
{
	return mtrand_rand_u32(&rr_randstate);
}

void RR_BeginSession (char *mapname, qboolean closeInput)
{
	int i;

	if(closeInput)
		RR_CloseInput_f();

	memset(&rr_sessiondata, 0, sizeof(rr_sessiondata));
	rr_sessiondata.version = RR_Q1_SESSIONDATA_VERSION;

	rr_sessiondata.serverflags = svs.serverflags;
	for(i=0;i<NUM_SPAWN_PARMS;i++)
		rr_sessiondata.spawnparms[i] = svs.clients[0].spawn_parms[i];
	rr_sessiondata.skill = skill.value;
	strcpy(rr_sessiondata.mapname, mapname);
	rr_sessiondata.initialSeed = RR_Hex32(rr_randomseed.string);

	mtrand_seed(&rr_randstate, rr_sessiondata.initialSeed);
}

void RR_Init ()
{
	Cmd_AddCommand ("rr_opensession", RR_OpenSession_f);
	Cmd_AddCommand ("rr_newsession", RR_NewSession_f);
	Cmd_AddCommand ("rr_savestate", RR_SaveState_f);
	Cmd_AddCommand ("rr_loadstate", RR_LoadState_f);
	Cmd_AddCommand ("rr_openinput", RR_OpenInput_f);
	Cmd_AddCommand ("rr_closeinput", RR_CloseInput_f);

	Cvar_RegisterVariable (&rr_frametime);
	Cvar_RegisterVariable (&rr_timescale);
	Cvar_RegisterVariable (&rr_framelimit);
	Cvar_RegisterVariable (&rr_randomseed);
	Cvar_RegisterVariable (&rr_dead);
	Cvar_RegisterVariable (&rr_nomapchange);
	Cvar_RegisterVariable (&rr_machineid);
	Cvar_RegisterVariable (&rr_record);
	Cvar_RegisterVariable (&rr_stringpoolsize);
}
